<?php

if (!defined('WPO_VERSION')) die('No direct access allowed');

/**
 * This class invokes optimiazations. The optimizations themselves live in the 'optimizations' subdirectory of the plugin.  The proper way to obtain access to the instance is via WP_Optimize()->get_optimizer()
 */
class WP_Optimizer {

	/**
	 * Returns singleton instance object
	 *
	 * @return WP_Optimizer Returns `WP_Optimizer` object
	 */
	public static function instance() {
		static $_instance = null;
		if (null === $_instance) {
			$_instance = new self();
		}
		return $_instance;
	}
	
	/**
	 * Gets retention information
	 *
	 * @return array
	 */
	public function get_retain_info() {
	
		$options = WP_Optimize()->get_options();
	
		$retain_enabled = $options->get_option('retention-enabled', 'false');
		$retain_period = (($retain_enabled) ? $options->get_option('retention-period', '2') : null);

		return array($retain_enabled, $retain_period);
	}

	/**
	 * Get data retention options
	 *
	 * @return array
	 */
	public function get_revisions_retain_info() {
		$options = WP_Optimize()->get_options();

		$revisions_retention_enabled = $options->get_option('revisions-retention-enabled', 'false');
		$revisions_retention_count = $revisions_retention_enabled ? $options->get_option('revisions-retention-count', '2') : null;

		return array($revisions_retention_enabled, $revisions_retention_count);
	}
	
	/**
	 * Gets a list of optimizations
	 *
	 * @return array
	 */
	public function get_optimizations_list() {
	
		$optimizations = array();
		
		$optimizations_dir = WPO_PLUGIN_MAIN_PATH.'optimizations';
		
		if ($dh = opendir($optimizations_dir)) {
			while (($file = readdir($dh)) !== false) {
				if ('.' === $file || '..' === $file || '.php' !== substr($file, -4, 4) || !is_file($optimizations_dir.'/'.$file) || 'inactive-' === substr($file, 0, 9)) continue;
				$optimizations[] = substr($file, 0, (strlen($file) - 4));
			}
			closedir($dh);
		}
		
		$filtered_optimizations = apply_filters('wp_optimize_get_optimizations_list', $optimizations);
		return is_array($filtered_optimizations) ? $filtered_optimizations : $optimizations;
	}
	
	/**
	 * Currently, there is only one sort rule (so, the parameter's value is ignored)
	 *
	 * @param  array  $optimizations An array of optimizations (i.e. WP_Optimization instances).
	 * @param  string $sort_on       Specify sort.
	 *
	 * @return array
	 */
	public function sort_optimizations($optimizations, $sort_on = 'ui_sort_order') {
		if ('run_sort_order' === $sort_on) {
			uasort($optimizations, array($this, 'sort_optimizations_run_traditional'));
		} else {
			uasort($optimizations, array($this, 'sort_optimizations_ui_traditional'));
		}
		return $optimizations;
	}
	
	/**
	 * Sorts optimizations based on UI order
	 *
	 * @param WP_Optimization $a
	 * @param WP_Optimization $b
	 * @return int
	 */
	public function sort_optimizations_ui_traditional($a, $b) {
		return $this->sort_optimizations_traditional($a, $b);
	}
	
	/**
	 * Sorts optimizations based on Run order
	 *
	 * @param WP_Optimization $a
	 * @param WP_Optimization $b
	 * @return int
	 */
	public function sort_optimizations_run_traditional($a, $b) {
		return $this->sort_optimizations_traditional($a, $b, 'run_sort_order');
	}
	
	/**
	 * Sorting list of optimization based on given order
	 *
	 * @param WP_Optimization $a
	 * @param WP_Optimization $b
	 * @param string          $sort_on
	 * @return int
	 */
	public function sort_optimizations_traditional($a, $b, $sort_on = 'ui_sort_order') {
		$sort_order_a = empty($a->$sort_on) ? 0 : $a->$sort_on;
		$sort_order_b = empty($b->$sort_on) ? 0 : $b->$sort_on;
	
		if ($sort_order_a === $sort_order_b) return 0;
		
		return ($sort_order_a < $sort_order_b) ? -1 : 1;
	}
	
	/**
	 * This method returns an array of available optimizations.
	 * Each array key is an optimization ID, and the value is an object,
	 * as returned by get_optimization()
	 *
	 * @return array array of optimizations
	 */
	public function get_optimizations() {
	
		$optimizations = $this->get_optimizations_list();
	
		$optimization_objects = array();
		
		foreach ($optimizations as $optimization) {
			$optimization_object = $this->get_optimization($optimization);
			if (is_wp_error($optimization_object)) {
				WP_Optimize()->log('Failed to load optimization ' . $optimization . ' - ' . $optimization_object->get_error_message());
			} else {
				$optimization_objects[$optimization] = $optimization_object;
			}
		}
	
		$filtered_optimizations = apply_filters('wp_optimize_get_optimizations', $optimization_objects);
		return is_array($filtered_optimizations) ? $filtered_optimizations : $optimization_objects;
	
	}
	
	/**
	 * This method returns an object for a specific optimization.
	 *
	 * @param  string|object $which_optimization An optimization ID.
	 * @param  array         $data               An array of anny options $data.
	 * @return array|WP_Error                    WP_Error Will return the optimization, or a WP_Error object if it was not found.
	 */
	public function get_optimization($which_optimization, $data = array()) {

		$optimization_class = apply_filters('wp_optimize_optimization_class', 'WP_Optimization_'.$which_optimization);
		
		if (!class_exists('WP_Optimization')) include_once(WPO_PLUGIN_MAIN_PATH.'includes/class-wp-optimization.php');
	
		if (!class_exists($optimization_class)) {
			$optimization_file = WPO_PLUGIN_MAIN_PATH.'optimizations/'.$which_optimization.'.php';
			$class_file = apply_filters('wp_optimize_optimization_class_file', $optimization_file);
			if (!preg_match('/^[a-z]+$/', $which_optimization) || !file_exists($class_file)) {
				return new WP_Error('no_such_optimization', __('No such optimization', 'wp-optimize'), $which_optimization);
			}
			
			include_once($class_file);
			
			if (!class_exists($optimization_class)) {
				return new WP_Error('no_such_optimization', __('No such optimization', 'wp-optimize'), $which_optimization);
			}
		}

		// set sites option for Multisite cron job.
		if (defined('DOING_CRON') && DOING_CRON && is_multisite()) {
			$options = WP_Optimize()->get_options();
			$data['optimization_sites'] = $options->get_option('wpo-sites-cron', array('all'));
		}
		
		$optimization = new $optimization_class($data);
		
		return $optimization;
	
	}

	/**
	 * The method to call to perform an optimization.
	 *
	 * @param  string|object $which_optimization An optimization ID, or a WP_Optimization object.
	 * @return object                            Array of results from the optimization.
	 */
	public function do_optimization($which_optimization) {
		
		$optimization = (is_object($which_optimization) && is_a($which_optimization, 'WP_Optimization')) ? $which_optimization : $this->get_optimization($which_optimization);

		if (is_wp_error($optimization)) {
			WP_Optimize()->log('Error occurred. Unknown optimization.');
			return $optimization;
		}

		WP_Optimize()->change_time_limit();

		$optimization->init();
	
		if (apply_filters('wp_optimize_do_optimization', true, $which_optimization, $optimization)) {

			$optimization->before_optimize();

			if ($optimization->run_multisite) {
				foreach ($optimization->blogs_ids as $blog_id) {
					$optimization->switch_to_blog($blog_id);
					$optimization->optimize();
					$optimization->restore_current_blog();
				}
			} else {
				$optimization->optimize();
			}

			$optimization->after_optimize();

		}
		
		do_action('wp_optimize_after_optimization', $which_optimization, $optimization);

		$results = $optimization->get_results();
			
		return $results;
	}
	
	/**
	 * The method to call to get information about an optimization.
	 * As with do_optimization, it is somewhat modelled after the template interface
	 *
	 * @param  string|object $which_optimization An optimization ID, or a WP_Optimization object.
	 * @return object                             returns the optimization information
	 */
	public function get_optimization_info($which_optimization) {
	
		$optimization = (is_object($which_optimization) && is_a($which_optimization, 'WP_Optimization')) ? $which_optimization : $this->get_optimization($which_optimization);
		
		if (is_wp_error($optimization)) return $optimization;

		WP_Optimize()->change_time_limit();

		$optimization->before_get_info();

		if ($optimization->run_multisite) {
			foreach ($optimization->blogs_ids as $blog_id) {
				$optimization->switch_to_blog($blog_id);
				$optimization->get_info();
				$optimization->restore_current_blog();
			}
		} else {
			$optimization->get_info();
		}

		$optimization->after_get_info();

		return $optimization->get_results();
	}
	
	/**
	 * This runs the list of optimizations.
	 *
	 * @param  array  $optimization_options Whether to do an optimization depends on what keys are set (legacy - can be changed hopefully).
	 * @param  string $which_option         Specify which option.
	 * @return array                        Returns an array of result objects.
	 */
	public function do_optimizations($optimization_options, $which_option = 'dom') {
	
		$results = array();
		
		if (empty($optimization_options)) return $results;
	
		$optimizations = $this->sort_optimizations($this->get_optimizations(), 'run_sort_order');

		foreach ($optimizations as $optimization_id => $optimization) {
			$option_id = call_user_func(array($optimization, 'get_'.$which_option.'_id'));
			
			if (isset($optimization_options[$option_id])) {
				// if options saved as a string then compare with string (for support different versions)
				if ('false' === $optimization_options[$option_id]) continue;

				if ('auto' === $which_option && empty($optimization->available_for_auto)) continue;

				WP_Optimize()->change_time_limit();

				$results[$optimization_id] = $this->do_optimization($optimization);
			}
		}

		// Run action after all optimizations completed.
		do_action('wp_optimize_after_optimizations');

		return $results;
		
	}
	
	public function get_table_prefix($allow_override = false) {
		$wpdb = $GLOBALS['wpdb'];
		if (is_multisite() && !defined('MULTISITE')) {
			// In this case (which should only be possible on installations upgraded from pre WP 3.0 WPMU), $wpdb->get_blog_prefix() cannot be made to return the right thing. $wpdb->base_prefix is not explicitly marked as public, so we prefer to use get_blog_prefix if we can, for future compatibility.
			$prefix = $wpdb->base_prefix;
		} else {
			$prefix = $wpdb->get_blog_prefix(0);
		}
		return ($allow_override) ? apply_filters('wp_optimize_get_table_prefix', $prefix) : $prefix;
	}

	/**
	 * Returns information about database tables.
	 *
	 * @param bool $update refresh or no cached data
	 *
	 * @return mixed
	 */
	public function get_tables($update = false) {
		static $tables_info = null;

		if (false === $update && null !== $tables_info) return $tables_info;

		$wpo_db_info = WP_Optimize()->get_db_info();

		$table_status = $wpo_db_info->get_show_table_status($update);

		// Filter on the site's DB prefix (was not done in releases up to 1.9.1).
		$table_prefix = $this->get_table_prefix();
		
		if (is_array($table_status)) {

			$corrupted_tables_count = 0;
			$array_index_was_unset = false;

			foreach ($table_status as $index => $table) {
				$table_name = $table->Name;
				
				$include_table = (0 === stripos($table_name, $table_prefix));
				
				$include_table = apply_filters('wp_optimize_get_tables_include_table', $include_table, $table_name, $table_prefix);

				if (!$include_table && '' !== $table_prefix) {
					unset($table_status[$index]);
					$array_index_was_unset = true;
					continue;
				}

				$table_status[$index]->Engine = $wpo_db_info->get_table_type($table_name);

				$table_status[$index]->is_optimizable = $wpo_db_info->is_table_optimizable($table_name);
				$table_status[$index]->is_type_supported = $wpo_db_info->is_table_type_optimize_supported($table_name);
				// add information about corrupted tables.
				$is_needing_repair = $wpo_db_info->is_table_needing_repair($table_name);
				$table_status[$index]->is_needing_repair = $is_needing_repair;
				if ($is_needing_repair) $corrupted_tables_count++;

				$table_status[$index] = $this->join_plugin_information($table_name, $table_status[$index]);

				$table_status[$index]->blog_id = $wpo_db_info->get_table_blog_id($table_name);
			}
			if ($array_index_was_unset) {
				// Reset array keys after removing elements
				$table_status = array_values($table_status);
			}

			WP_Optimize()->get_options()->update_option('corrupted-tables-count', $corrupted_tables_count);
		}

		$tables_info = apply_filters('wp_optimize_get_tables', $table_status);
		return $tables_info;
	}

	/**
	 * Returns information about single table by table name.
	 *
	 * @param string $table_name table name
	 * @return object|boolean table information object.
	 */
	public function get_table($table_name) {
	
		$db_info = WP_Optimize()->get_db_info();
	
		$table = $db_info->get_table_status($table_name);
		
		if (false === $table) return false;

		$table->is_optimizable = $db_info->is_table_optimizable($table_name);
		$table->is_type_supported = $db_info->is_table_type_optimize_supported($table_name);
		$table->is_needing_repair = $db_info->is_table_needing_repair($table_name);

		// add information about plugins.
		$table = $this->join_plugin_information($table_name, $table);

		$table = apply_filters('wp_optimize_get_table', $table);
		return $table;
	}

	/**
	 * Add information about relationship database tables with plugins.
	 *
	 * @param string $table_name
	 * @param object $table_obj
	 *
	 * @return object
	 */
	public function join_plugin_information($table_name, $table_obj) {
		// set can be removed flag.
		$can_be_removed = false;
		// set WP core table flag.
		$wp_core_table = false;
		// set WP actionscheduler table flag.
		$wp_actionscheduler_table = (false !== stripos($table_name, 'actionscheduler_'));
		// add information about using table by any of installed plugins.
		$table_obj->is_using = WP_Optimize()->get_db_info()->is_table_using_by_plugin($table_name);
		// if table belongs to any plugin then add plugins status.
		$plugins = WP_Optimize()->get_db_info()->get_table_plugin($table_name);

		$plugin_status = array();

		if (false !== $plugins) {
			// if belongs to any of plugin then we can remove table if plugin not active.
			$can_be_removed = true;

			foreach ($plugins as $plugin) {
				$status = WP_Optimize()->get_db_info()->get_plugin_status($plugin);

				if (__('WordPress core', 'wp-optimize') === $plugin) $wp_core_table = true;
				// if plugin is active then we can't remove.
				if ($wp_core_table || $status['active'] || $wp_actionscheduler_table) $can_be_removed = false;

				if ($status['installed'] || $status['active'] || !$table_obj->is_using) {
					$plugin_status[] = array(
						'plugin' => $plugin,
						'status' => $status,
					);
				}
			}
		}

		$table_obj->plugin_status = $plugin_status;
		$table_obj->wp_core_table = $wp_core_table;
		$table_obj->can_be_removed = $can_be_removed;

		return $table_obj;
	}

	/**
	 * This function grabs a list of tables
	 * and information regarding each table and returns
	 * the results to optimizations-table.php and optimizationstable.php
	 *
	 * @param int $blog_id filter tables by prefix
	 *
	 * @return array - an array of data such as table list, innodb info and data free
	 */
	public function get_table_information($blog_id = 0) {
		// Get table information.
		$tablesstatus = $this->get_tables();

		// Set defaults.
		$table_information = array();
		$table_information['total_gain'] = 0;
		$table_information['inno_db_tables'] = 0;
		$table_information['non_inno_db_tables'] = 0;
		$table_information['table_list'] = '';
		$table_information['is_optimizable'] = true;

		// Make a list of tables to optimize.
		foreach ($tablesstatus as $each_table) {
			// if $blog_id is set then filter tables
			if ($blog_id > 0 && $blog_id !== $each_table->blog_id) continue;

			$table_information['table_list'] .= $each_table->Name.'|';

			// check if table type supported.
			if (!$each_table->is_type_supported) continue;

			// check if table is optimizable.
			if (!$each_table->is_optimizable) {
				$table_information['is_optimizable'] = false;
			}

			// calculate total gain value.
			$table_information['total_gain'] += $each_table->Data_free;

			// count InnoDB tables.
			if ('InnoDB' === $each_table->Engine) {
				$table_information['inno_db_tables']++;
			} else {
				$table_information['non_inno_db_tables']++;
			}
		}

		return $table_information;
	}
	
	/**
	 * What sort of linkback to enable or disable: valid values are 'trackbacks' or 'comments', and whether to enable or disable.
	 *
	 * @param string  $type   Specify the type of linkbacks.
	 * @param boolean $enable If it is enabled or disabled.
	 */
	public function enable_linkbacks($type, $enable = true) {
	
		$wpdb = $GLOBALS['wpdb'];
		$wpo_options = WP_Optimize()->get_options();
		
		$new_status = $enable ? 'open' : 'closed';
		
		switch ($type) {
			case "trackbacks":
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE `{$wpdb->posts}` SET ping_status = %s WHERE post_status = %s AND post_type = %s",
					$new_status,
					'publish',
					'post'
				)
			);
				break;

			case "comments":
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE `{$wpdb->posts}` SET comment_status = %s WHERE post_status = %s AND post_type = %s",
					$new_status,
					'publish',
					'post'
				)
			);
				break;

			default:
				break;
		}
		$wpo_options->update_option($type.'_action', array('action' => $enable, 'timestamp' => time()));

	}
	
	/**
	 * This function will return total database size and a possible gain of db in KB.
	 *
	 * @param boolean $update - Whether to update the values or not
	 * @return array total db size gained.
	 */
	public function get_current_db_size($update = false) {
		
		if (!$update && $db_size = get_transient('wpo_get_current_db_size')) {
			return $db_size;
		}

		$wp_optimize = WP_Optimize();

		$total_gain = 0;
		$total_size = 0;
		$row_usage = 0;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $row_usage Used in the foreach below
		$data_usage = 0;
		$index_usage = 0;
		$overhead_usage = 0;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $overhead_usage Used in the foreach below
		
		$tablesstatus = $this->get_tables();

		foreach ($tablesstatus as $tablestatus) {
			$row_usage += $tablestatus->Rows;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $row_usage declared up above
			$total_gain += $tablestatus->Data_free;
			$data_usage += $tablestatus->Data_length;
			$index_usage += $tablestatus->Index_length;

			if ('InnoDB' !== $tablestatus->Engine) {
				$overhead_usage += $tablestatus->Data_free;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $overhead_usage declared up above
				$total_gain += $tablestatus->Data_free;
			}
		}

		$total_size = ($data_usage + $index_usage);
		$db_size = array($wp_optimize->format_size($total_size), $wp_optimize->format_size($total_gain));
		set_transient('wpo_get_current_db_size', $db_size, 3600);
		return $db_size;
	}

	/**
	 * This function will return total saved data in KB.
	 *
	 * @param  string $current How big the data is.
	 * @return string          Returns new total value.
	 */
	public function update_total_cleaned($current) {

		$options = WP_Optimize()->get_options();
	
		$previously_saved = floatval($options->get_option('total-cleaned', '0'));
		$converted_current = floatval($current);

		$total_now = strval($previously_saved + $converted_current);

		$options->update_option('total-cleaned', $total_now);

		return $total_now;
	}
	
	
	/**
	 * Trackback, Comments actions
	 *
	 * @param array $options
	 * @return array
	 */
	public function trackback_comment_actions($options) {
	
		$output = array();
		$messages = array();
	
		if (isset($options['comments'])) {
			if (!$options['comments']) {
				$this->enable_linkbacks('comments', false);
				$output[] = __('Comments have now been disabled on all current and previously published posts.', 'wp-optimize');
				// translators: %s is a date and time
				$messages[] =  sprintf(__('All comments on existing posts were disabled at %s.', 'wp-optimize'), WP_Optimize()->format_date_time(time()));
			} else {
				$this->enable_linkbacks('comments');
				$output[] = __('Comments have now been enabled on all current and previously published posts.', 'wp-optimize');
				// translators: %s is a date and time
				$messages[] =  sprintf(__('All comments on existing posts were enabled at %s.', 'wp-optimize'), WP_Optimize()->format_date_time(time()));
			}
		}
		
		if (isset($options['trackbacks'])) {
			if (!$options['trackbacks']) {
				$this->enable_linkbacks('trackbacks', false);
				$output[] = __('Trackbacks have now been disabled on all current and previously published posts.', 'wp-optimize');
				// translators: %s is a date and time
				$messages[] =  sprintf(__('All trackbacks on existing posts were disabled at %s.', 'wp-optimize'), WP_Optimize()->format_date_time(time()));
			} else {
				$this->enable_linkbacks('trackbacks');
				$output[] = __('Trackbacks have now been enabled on all current and previously published posts.', 'wp-optimize');
				// translators: %s is a date and time
				$messages[] =  sprintf(__('All trackbacks on existing posts were enabled at %s.', 'wp-optimize'), WP_Optimize()->format_date_time(time()));
			}
		}
		
		return array('output' => $output,'messages' => $messages);
	}

	/**
	 * Whether InnoDB tables require confirmation to be optimized
	 *
	 * @return boolean
	 */
	public function show_innodb_force_optimize() {
		$tablesstatus = $this->get_table_information();
		return false === $tablesstatus['is_optimizable'] && $tablesstatus['inno_db_tables'] > 0;
	}
}
